﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Loki.Engine;
using Loki.Utils;

namespace Loki.Model
{
    public class EntityLinkContainer<TEntityKey> : LokiObject, IDisposable, IContainer<IEntityLink<TEntityKey>>, IEntityLinkContainer<TEntityKey>
    {
        #region Disposable
        /// <summary>
        /// Releases all resources used by an instance of the <see cref="EntityContainer" /> class.
        /// </summary>
        /// <remarks>
        /// This method calls the virtual <see cref="Dispose(bool)" /> method, passing in <strong>true</strong>, and then suppresses 
        /// finalization of the instance.
        /// </remarks>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged resources before an instance of the <see cref="EntityContainer" /> class is reclaimed by garbage collection.
        /// </summary>
        /// <remarks>
        /// This method releases unmanaged resources by calling the virtual <see cref="Dispose(bool)" /> method, passing in <strong>false</strong>.
        /// </remarks>
        ~EntityLinkContainer()
        {
            Dispose(false);
        }

        /// <summary>
        /// Releases the unmanaged resources used by an instance of the <see cref="EntityContainer" /> class and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing"><strong>true</strong> to release both managed and unmanaged resources; <strong>false</strong> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {

        }
        #endregion

        private static NotifyCollectionChangedEventArgs _ResetArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);

        private LinkContainerConfiguration<TEntityKey> _Config;
        private bool _Loaded = false;
        private object _Lock = new object();
        private Func<IEnumerable<IEntityLink<TEntityKey>>> _FullLoader;
        private Func<TEntityKey, IEnumerable<IEntityLink<TEntityKey>>> _LeftLoader;
        private Func<TEntityKey, IEnumerable<IEntityLink<TEntityKey>>> _RightLoader;

        private UniqueKeyMapper<IEntityLink<TEntityKey>, IEntityLink<TEntityKey>> _PK;
        private IndexedKeyMapper<IEntityLink<TEntityKey>, TEntityKey> _LeftIndex;
        private IndexedKeyMapper<IEntityLink<TEntityKey>, TEntityKey> _RightIndex;

        public EntityLinkContainer(LinkContainerConfiguration<TEntityKey> P_Config)
        {
            _Config = P_Config;
            LinkEqualityComparer<TEntityKey> _Comparer = new LinkEqualityComparer<TEntityKey>();

            _FullLoader = _Config.FullLoader.Compile();
            _LeftLoader = _Config.LeftLoader.Compile();
            _RightLoader = _Config.RightLoader.Compile();

            _LeftIndex = new IndexedKeyMapper<IEntityLink<TEntityKey>, TEntityKey>(
                new IndexConfiguration<IEntityLink<TEntityKey>, TEntityKey>()
                {
                    Indexer = x => x.Left,
                    Loader = x => _LeftLoader((TEntityKey)x)
                }, _Comparer);

            _RightIndex = new IndexedKeyMapper<IEntityLink<TEntityKey>, TEntityKey>(
                new IndexConfiguration<IEntityLink<TEntityKey>, TEntityKey>()
                {
                    Indexer = x => x.Right,
                    Loader = x => _RightLoader((TEntityKey)x)
                }, _Comparer);

            _PK = new UniqueKeyMapper<IEntityLink<TEntityKey>, IEntityLink<TEntityKey>>(
                x => x, _Comparer);
        }

        public void FullLoad()
        {
            lock (_Lock)
            {
                _EnsureFullLoad();
            }
        }

        public void Reset()
        {
            lock (_Lock)
            {
                _Reset();
            }
        }

        private void _Reset()
        {
            _PK.Reset();
            _LeftIndex.Reset();
            _RightIndex.Reset();
            _Loaded = false;

            OnCollectionChanged(_ResetArgs);
        }

        public IEnumerable<IEntityLink<TEntityKey>> GetAllByLeft(TEntityKey P_LeftValue)
        {
            lock (_Lock)
            {
                _EnsureLeftLoaded(P_LeftValue);
                return _LeftIndex.GetAllByKey(P_LeftValue);
            }
        }

        public IEnumerable<IEntityLink<TEntityKey>> GetAllByRight(TEntityKey P_RightValue)
        {
            lock (_Lock)
            {
                _EnsureRightLoaded(P_RightValue);
                return _RightIndex.GetAllByKey(P_RightValue);
            }
        }

        public bool Register(IEntityLink<TEntityKey> P_Entity)
        {
            lock (_Lock)
            {
                return A_AddOrReplaceItem(P_Entity);
            }
        }

        public void Remove(IEntityLink<TEntityKey> P_Entity)
        {
            lock (_Lock)
            {
                _Remove(P_Entity);
            }
        }

        public IEnumerator<IEntityLink<TEntityKey>> GetEnumerator()
        {
            lock (_Lock)
            {
                _EnsureFullLoad();
                foreach (IEntityLink<TEntityKey> L_Item in _PK)
                {
                    if (Object.Equals(_Purify(L_Item), L_Item))
                        yield return L_Item;
                }
            }
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        private void _Remove(IEntityLink<TEntityKey> P_Entity)
        {
            // remove
            _PK.Remove(P_Entity);
            _LeftIndex.Remove(P_Entity);
            _RightIndex.Register(P_Entity);

            Type L_Type = typeof(IEntityLink<TEntityKey>);
            Log.InfoFormat("Removing entity link {0}-{1} of L_Type {2}", P_Entity.Left, P_Entity.Right, L_Type.Name);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, P_Entity));

        }

        private IEntityLink<TEntityKey> _Purify(IEntityLink<TEntityKey> P_Class)
        {
            /*if (_LeftIndex.IsInvalidated(P_Class.Left) || _RightIndex.IsInvalidated(P_Class.Right))
                return null;
            else*/
            return P_Class;
        }

        private bool _IsLoaded(IEntityLink<TEntityKey> P_Entity)
        {
            return _PK.IsLoaded(P_Entity) && Object.Equals(_Purify(P_Entity), P_Entity);
        }

        private bool A_AddOrReplaceItem(IEntityLink<TEntityKey> P_Entity)
        {
            bool L_AlreadyPresent;
            NotifyCollectionChangedEventArgs e = null;

            L_AlreadyPresent = _IsLoaded(P_Entity);
            if (!L_AlreadyPresent)
            {
                e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, P_Entity);
            }

            // registering
            _IndexItem(P_Entity);

            OnCollectionChanged(e);
            return !L_AlreadyPresent;
        }

        private void _IndexItem(IEntityLink<TEntityKey> P_Entity)
        {
            _PK.Register(P_Entity);
            _LeftIndex.Register(P_Entity);
            _RightIndex.Register(P_Entity);
        }

        public void InvalidateLeftKey(TEntityKey P_FkValue)
        {
            /* lock (_Lock)
             {
                 _LeftIndex.Invalidate(P_FkValue);
                 _Loaded = false;
             }*/
        }

        public void InvalidateRightKey(TEntityKey P_FkValue)
        {
            /*lock (_Lock)
            {
                _RightIndex.Invalidate(P_FkValue);
                _Loaded = false;
            }*/
        }

        /// <summary>
        /// Check or load all.
        /// </summary>
        private void _EnsureFullLoad()
        {
            // global load if not loaded
            if (!_Loaded)
            {
                foreach (IEntityLink<TEntityKey> L_Entity in _FullLoader())
                {
                    _IndexItem(L_Entity);
                }

                _Loaded = true;
                OnCollectionChanged(_ResetArgs);
            }
        }

        /// <summary>
        /// Checks (and load if not) if the entities with the specified value on the specified member are loaded.
        /// </summary>
        /// <param name="P_Fkvalue">The fkvalue</param>
        private void _EnsureLeftLoaded(TEntityKey P_Fkvalue)
        {
            // check if the container is full loaded
            if (!(_Loaded || _LeftIndex.IsLoaded(P_Fkvalue)))
            {
                // check if a the key is loaded and if a specific loader is available
                if (!_LeftIndex.IsLoaded(P_Fkvalue))
                {
                    try
                    {
                        IEnumerable<IEntityLink<TEntityKey>> _Entities = _LeftLoader(P_Fkvalue);
                        foreach (IEntityLink<TEntityKey> L_Entity in _Entities)
                        {
                            _IndexItem(L_Entity);
                        }
                        _LeftIndex.MarkAsLoaded(P_Fkvalue);

                        OnCollectionChanged(_ResetArgs);
                    }
                    catch
                    {
                        // TODO : improve
                        throw;
                        // throw BuildError(_Config.LoadingErrorCode, ex);
                    }
                }
                else
                {
                    // no specific loader defined
                    _EnsureFullLoad();
                }
            }
        }

        private void _EnsureRightLoaded(TEntityKey P_Fkvalue)
        {
            // check if the container is full loaded
            if (!(_Loaded || _RightIndex.IsLoaded(P_Fkvalue)))
            {
                // check if a the key is loaded and if a specific loader is available
                if (!_LeftIndex.IsLoaded(P_Fkvalue))
                {
                    try
                    {
                        IEnumerable<IEntityLink<TEntityKey>> _Entities = _RightLoader(P_Fkvalue);
                        foreach (IEntityLink<TEntityKey> L_Entity in _Entities)
                        {
                            _IndexItem(L_Entity);
                        }
                        _LeftIndex.MarkAsLoaded(P_Fkvalue);

                        OnCollectionChanged(_ResetArgs);
                    }
                    catch
                    {
                        // TODO : improve
                        throw;
                        // throw BuildError(_Config.LoadingErrorCode, ex);
                    }
                }
                else
                {
                    // no specific loader defined
                    _EnsureFullLoad();
                }
            }
        }

        #region Events
        #region Collection Changed
        private readonly object CollectionChangedEventLock = new object();
        private NotifyCollectionChangedEventHandler CollectionChangedEvent;

        public event NotifyCollectionChangedEventHandler CollectionChanged
        {
            add
            {
                lock (CollectionChangedEventLock)
                {
                    CollectionChangedEvent += value;
                }
            }
            remove
            {
                lock (CollectionChangedEventLock)
                {
                    CollectionChangedEvent -= value;
                }
            }
        }

        protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            NotifyCollectionChangedEventHandler handler = null;

            lock (CollectionChangedEventLock)
            {
                handler = CollectionChangedEvent;

                if (handler == null)
                    return;
            }

            handler(this, e);
        }

        #endregion
        #endregion
    }
}
